|     java thinking in java access control package   |    

摘要

这章就Java对代码块的组织做了一个详细的描述。其中package包的内容,可以参看我的《Mac上自搭舒服的Java环境,别总是用IDE》这篇文章。

后一部分详细介绍了public,protected,default和private四种不同的访问权限等级。描述了一种以private为字段基本面,default包可见为基本方法调用权限的谨慎的编程风格。特别还举例说明了singleton设计模式,通过私有化构造器,拒绝外部实例化。全局只留唯一的静态对象,而且对这个静态对象的操作也只留下一组特定的接口方法。管理非常到位。

这里关于package以及CLASSPATH的内容,我在《Mac上自搭舒服的Java环境,别总是用IDE》里已经系统介绍过,这里不再重复。

练习1

Create a class in a package. Create an instance of your class outside of that package. 第五章关于finalize()的练习里,我的HowlingDog类从第三章的Dog类继承过来,当时就在导入了第三章的package。

package com.ciaoshen.thinkinjava.chapter5;

import java.util.*;
import com.ciaoshen.thinkinjava.chapter3.*;


/**
 *  Inherit the Dog class in Chapter 3
 */
public class HowlingDog extends com.ciaoshen.thinkinjava.chapter3.Dog {

    //tag to notice if the dog howls
    public boolean itHowls = false;

    public HowlingDog(String inName, String inSays){
        super(inName,inSays);
    }
    ...
    ...
}

静态import

比如说我的工具包com.ciaoshen.util里有静态方法print()来实现System.out.println()的功能。当我静态导入我的工具包以后,就可以用print()代替System.out.println()了。

import static com.ciaoshen.util

练习 2

Take the code fragments in this section and turn them into a program, and verify that collisions do in fact occur.

package com.ciaoshen.thinkinjava.chapter6;

import java.util.*;
//chapter6下面的debug和debugoff包有两个同名类DebugClass。导致冲突。
import com.ciaoshen.thinkinjava.chapter6.*;

public class SameNameClass{

    /**
     *  main method
     *  @param args void
     */
    public static void main(String[] args){
        DebugClass.debug();     //  - 1 -
        com.ciaoshen.thinkinjava.chapter6.debug.DebugClass.debug();     //  - 2 -
    }
}

Output:

//执行 - 1 -,编译器发现同名类DebugClass,报错。
/Users/Wei/java/com/ciaoshen/thinkinjava/chapter6/SameNameClass.java:20: error: cannot find symbol
        DebugClass.debug();     //  - 1 -
        ^
  symbol:   variable DebugClass
  location: class SameNameClass
1 error


//执行 - 2 -,正常
I am com.ciaoshen.thinkinjava.chapter6.debug.DebugClass.debug()

访问权限

这是一个很严肃的话题。刚学Java的时候还没顾得上管这个,程序能跑就行。但工业级别的代码,没有访问控制肯定要乱套。

我觉得设访问权限,一定要有思路,一个个类,成员去考虑设什么权限肯定是不行的。一个整体的头绪和层次能让这个问题简单很多。首先,全public肯定是太不安全,全private根本调用不动。

书里推荐的一个方法不错,我看官方类库都在用:也就是变量尽量保护起来,然后通过accesor(访问器)和mutator(变异器)的get和set公开方法访问。这样比较优雅。

作者推荐在源代码中,将成员按可见度由高到低的排列方法,更易读,也让其他程序员和客户,关注更需要他们关注的部分。

//类中先列public再protected再private成员
//这样的风格更易读
    public class OrganizedByAccess {
      public void pub1() { /* ... */ }
      public void pub2() { /* ... */ }
      public void pub3() { /* ... */ }
      private void priv1() { /* ... */ }
      private void priv2() { /* ... */ }
      private void priv3() { /* ... */ }
      private int i;

练习 4

Show that protected methods have package access but are not public. 调用两个相同的protectedDebug()方法,一个和调用类在同一个包里,另一个不是。

//I call two methods, one in the same package another not.
public static void main(String[] args){
    //in another package
    DebugClass.protectedDebug();
    //in the same package
    SameNameClass.protectedDebug();
}

//protectedDebug() method in the same package
//com.ciaoshen.thinkinjava.chapter6包
protected static void protectedDebug(){
    System.out.println("I am protectedDebug() method in chapter6.SameNameClass");
}

//protectedDebug() method in another package
//com.ciaoshen.thinkinjava.chapter6.debug包

    protected static void protectedDebug(){
        System.out.println("I am protectedDebug() method in chapter6.debug.DebugClass");
    }

调用结果:在同一个包里方法调用成功,不同包的调用失败。

/Users/Wei/java/com/ciaoshen/thinkinjava/chapter6/CallProtected.java:23: error: protectedDebug() has protected access in DebugClass
        DebugClass.protectedDebug();
                  ^
1 error
I am protectedDebug() method in chapter6.SameNameClass

练习 5

Create a class with public, private, protected, and package-access fields and method members. Create an object of this class and see what kind of compiler messages you get when you try to access all the class members. Be aware that classes in the same directory are part of the “default” package.

一个类中分别定义了三种public,protectd,private访问权限的成员方法。

///Users/Wei/java/com/ciaoshen/thinkinjava/chapter6/SameNameClass.java
    //public method
    public static void publicDebug(){
        System.out.println("I am publicDebug() method in chapter6.SameNameClass");
    }

    //protected method
    protected static void protectedDebug(){
        System.out.println("I am protectedDebug() method in chapter6.SameNameClass");
    }

    //private method
    private static void privateDebug(){
        System.out.println("I am privateDebug() method in chapter6.SameNameClass");
    }

同一个包里另外一个类调用这三种方法。

///Users/Wei/java/com/ciaoshen/thinkinjava/chapter6/CallProtected.java
        //exercise 5
        //in the same package
        SameNameClass.publicDebug();
        //in the same package
        SameNameClass.protectedDebug();
        //in the same package
        SameNameClass.privateDebug();

结果编译在privateDebug()方法这里通不过。证明同一个包可以访问public和protected但private不行。

/Users/Wei/java/com/ciaoshen/thinkinjava/chapter6/CallProtected.java:35: error: privateDebug() has private access in SameNameClass
        SameNameClass.privateDebug();
                     ^
1 error

练习 6

Create a class with protected data. Create a second class in the same file with a method that manipulates the protected data in the first class.

    protected static String protectedValue = "Protected Value";

    public static void main(String[] args){
        //exercise 6
        System.out.println(CallProtected.protectedValue + " is visited!");   
    }

//output: Protected Value is visited!

类的访问权限

可以直接设置整个类的访问权限。但类的访问权限只有两种public和默认(package-private)。是没有private和protected的。而且每个.java文件里只能有一个public class。

  1. public class:公开,谁都能访问。每个类里仅限一个。
  2. class:包访问权限。默认只有包内方法才能访问。

!!!注意:作者说每个.java文件里只能有一个public class,但实际不要把很多非公有类放到一个文件里。这样会导致”Auxiliary Class“问题。不是一个好的编程风格。正确的做法是,为每个类,创建一个单独的.java文件。下面是一个简单的测试: PublicClass.java文件里有两个类。一个公开,一个非公开。

package com.ciaoshen.thinkinjava.chapter7;
import java.util.*;

//My public class
public class PublicClass {
    //default constructor
    public PublicClass(){
        System.out.println("Hello, I am PublicClass.");
    }
}

//Non public class
//It should be package reachable
class PackageReachableClass {
    //default constructor
    PackageReachableClass(){
        System.out.println("Hi, I am PackageReachableClass.");
    }
}

我在同一个包的另一个文件InPackageClass.java文件里调用这两个类:

public class InPackageClass {

	/**
 	*  MAIN
		*  @param args void
 	*/
	public static void main(String[] args){
    	//pubic class can be reached from anywhere
    	PublicClass newPublicClass=new PublicClass();
    	//non-public-class should be accessable in the same package
    	PackageReachableClass newPackageReachableClass =new PackageReachableClass();
	}
}

系统会提出警告:Auxiliary Class(辅助类)不应该在他所在的.java文件外被调用。

/Users/Wei/java/com/ciaoshen/thinkinjava/chapter7/InPackageClass.java:22: warning: auxiliary class PackageReachableClass in ./com/ciaoshen/thinkinjava/chapter7/PublicClass.java should not be accessed from outside its own source file
        PackageReachableClass newPackageReachableClass =new PackageReachableClass();
        ^
/Users/Wei/java/com/ciaoshen/thinkinjava/chapter7/InPackageClass.java:22: warning: auxiliary class PackageReachableClass in ./com/ciaoshen/thinkinjava/chapter7/PublicClass.java should not be accessed from outside its own source file
        PackageReachableClass newPackageReachableClass =new PackageReachableClass();
                                                            ^
2 warnings
Hello, I am PublicClass.
Hi, I am PackageReachableClass.

在StackOverFlow里有专门的回答:一个java文件里塞多个非公开类,叫Auxiliary Class(辅助类),虽然包内也能调用,但这不是一个好的编程风格。最好为每个类都创建一个单独的java文件。 auxiliaryClass

非公开类中的公开成员

没有public修饰符的非公开类,他们里面的public成员,在包外能调用吗?又做了一个实验:

  1. 非公开类
  2. private字段
  3. public方法
  4. 还有一个public static公开静态方法 包外的类能调用吗?
package com.ciaoshen.thinkinjava.chapter5;
import java.util.*;
//非公开类,没有public修饰符
class InPackageTank {

    //公开构造函数
    public InPackageTank(String name){
        this.name=name;
    }

    //公开静态方法public static
    public static void show(){System.out.println("Show Time!");}

    //普通公开方法
    public void shot(){
        this.bullet --;
        if (this.bullet==0){
            this.isEmpty=true;
        }
    }

    //私有字段
    private boolean isEmpty = false;
    private int bullet = 100;
    private String name = new String();
}

另外一个包里,调用坦克类:

package com.ciaoshen.thinkinjava.chapter7;

import java.util.*;
import com.ciaoshen.thinkinjava.chapter5.*;

public class CallTank {

    /**
     *  MAIN
     *  @param args void
     */
    public static void main(String args[]){
        //静态方法,不实例化
        InPackageTank.show();

        //实例化
        Tank tank1 = new Tank("Tank1");
        System.out.println(tank1.bullet);
        tank1.shot();
        System.out.println(tank1.bullet);

    }
}

静态方法都调用不动:

/Users/Wei/java/com/ciaoshen/thinkinjava/chapter7/CallTank.java:21: error: InPackageTank is not public in com.ciaoshen.thinkinjava.chapter5; cannot be accessed from outside package
        InPackageTank.show();
        ^
1 error
Erreur : impossible de trouver ou charger la classe principale com.ciaoshen.thinkinjava.chapter7.CallTank

更不用说需要实例化的普通方法。

/Users/Wei/java/com/ciaoshen/thinkinjava/chapter7/CallTank.java:21: error: InPackageTank is not public in com.ciaoshen.thinkinjava.chapter5; cannot be accessed from outside package
        InPackageTank.show();
        ^
/Users/Wei/java/com/ciaoshen/thinkinjava/chapter7/CallTank.java:24: error: InPackageTank is not public in com.ciaoshen.thinkinjava.chapter5; cannot be accessed from outside package
        InPackageTank tank1 = new InPackageTank("Tank1");
        ^
/Users/Wei/java/com/ciaoshen/thinkinjava/chapter7/CallTank.java:24: error: InPackageTank is not public in com.ciaoshen.thinkinjava.chapter5; cannot be accessed from outside package
        InPackageTank tank1 = new InPackageTank("Tank1");
                                  ^
3 errors
Erreur : impossible de trouver ou charger la classe principale com.ciaoshen.thinkinjava.chapter7.CallTank

所以,包内可见类内的公开成员,还是只能包可见。
!!!注意:一个好的编程风格是:尽量少用包可见类。不得不用的时候,也要像对待公开类一样,保护好它的内部成员。保持字段私有,接口方法公开的原则。同样的原则还适用于给类加final修饰符的时候。这主要基于以下两个事实:

  1. 如果包内可见类被另一个公开类继承,那它内部成员的属性就开始具有意义了。包内可见类里面乱设访问限制就麻烦大了。
  2. 包内可见类经常被用来临时关闭一个类的可见性。必要的时候我们会重新加上public,或者去掉final,重启可见性,和可继承性。这时候,我们绝对不会想每次都重设所有成员的访问级别。所以一个好的习惯是:假装这个类是对外开放的,永远保持好内部所有的细节。

packagePrivateClass

类的隐身斗篷

在这里作者给出了一个我认为绝佳的Practice,怎么隐藏你的类。思路层次非常清楚:

  1. 类只有包可见。
  2. 包可见类里的成员,基本都设成private。尤其是构造器。这样没人能创建此类的对象。你的类别人根本没法实例化,也没法继承,它就像穿了一件隐身斗篷:DON’T TOUCH ME!!!
  3. 可以留两个static静态访问方法。在这个访问方法里,你就是上帝!可以设各种限制。比如加个计数器,实例数量超过阈值就拒绝再创建对象。更绝的,做成个singleton(单例器),保证heap堆里永远只有一个实例。哪天不高兴了,就把静态访问器关掉,闭门谢客。好拽。
普通静态访问器
//包访问类
class Soup1 {
  //私有构造器
  private Soup1() {}
  //静态访问器
  public static Soup1 makeSoup() {
    //返回实例前,可以做各种统计,控制
    return new Soup1();
  }
}
singleton(单例器)模式
//其他都和普通访问器相同,尽可能地私有化
class Soup2 {
  private Soup2() {}
  //全局唯一的静态实例
  private static Soup2 ps1 = new Soup2();
  //访问器只能访问这个唯一的静态实例。本宫给你们个皮球,你们玩儿去吧。
  public static Soup2 access() {
return ps1; }
  public void f() {}
}

练习 8

Following the form of the example Lunch.java, create a class called ConnectionManager that manages a fixed array of Connection objects. The client programmer must not be able to explicitly create Connection objects, but can only get them via a static method in ConnectionManager. When the ConnectionManager runs out of objects, it returns a null reference. Test the classes in main( ).

singleton的模式还是挺好玩的,这道题我把ConnectionManager做成了一个singleton模式。构造函数设为私有,用户无法初始化ConnectionManager的任何实例。唯一的访问途径是我的公开singleton()方法,返回全局唯一的一个ConnectionManager静态对象:theOnlyManagerConnection类被设置成本包可见,拒绝任何外部访问。

通过这道题,我体会到了权限控制的重要性。假设ConnectionManager类是一个游戏服务器管理用户网络连接的组件,如果一切成员都是public,那意味着其他程序员可以从其他组件中跳过ConnectionManager的控制,直接接触到我底层的每个Connection,可以随意地篡改连接的属性,甚至直接删除任意玩家已建立的连接。最直接的两个灾难性后果是:

  1. 只要任何一个模块不小心误操作底层connection,玩家的连接就会崩溃。
  2. 一旦我的ConnectionManager内部做任何细小的改动,比如connection的某个字段改个名字,其他程序员调用我数据的部分就无法正常工作。

设计ConnectionManager的目的,就是把底层connection的细节全部隐藏起来,只留下我想让大家操作的几个接口。这样也便于模块间的耦合。

另外singleton的好处是,强制全局只有theSingleManager这一个控制器的实例。保证所有对connection的操作都作用于theSingleManager这个全局唯一的静态实例。

/**
 *  ConnectionManager manage the Connections.
 *  @author wei.shen@iro.umontreal.ca
 *  @version 1.0
 */

package com.ciaoshen.thinkinjava.chapter6;

import java.util.*;
import java.text.DateFormat;
import java.text.SimpleDateFormat;


/**
 *  Only the ConnectionManager class is public visible.
 */
public class ConnectionManager {

    /*********************
     *  public methods
     *********************/
    //the only public Object available for users
    //the static interface of singleton pattern
    public static ConnectionManager singleton(){
        return theOnlyManager;
    }

    //reset the local IP
    public void setLocalIp(String nowIp){
        this.localIp=nowIp;
        System.out.println("Success! Now local IP is:   "+this.localIp);
    }

    //show everthing about the connection we have so far.
    //print the connection info line by line
    public void showConn(){
        //print the number of connection
        System.out.println("==============================");
        System.out.println("We have "+this.connNum+" connections now!   |");
        System.out.println("==============================");
        //print line by line
        for(Connection ele : this.connArray){
            System.out.println("||"+ele.toString()+"\t\t||");
            System.out.println("----------------------------------------------------------------------------------------------------------------------------------");

        }
    }


    //create a new connection and insert it into the table
    //check if the connection already exist before create it
    public void addConn(String clientIp){
        if(this.hasConn(this.localIp, clientIp)==-1){
            this.connArray.add(new Connection(this.idCounter, this.localIp, clientIp));
            System.out.println("Connection created!");
        } else {
            System.out.println("This connection already exist. Give me another IP please!");
            System.out.println(this.connArray.get(this.hasConn(this.localIp, clientIp)).toString());
        }
        this.connNum=this.connArray.size();
        this.idCounter ++;
    }

    //kill a connection if it exists
    public void killConn(int connId){
        if(this.hasId(connId)!=-1){
            System.out.println("This connection has been killed!");
            System.out.println(this.connArray.get(this.hasId(connId)).toString());

            this.connArray.remove(this.hasId(connId));
        } else {
            System.out.println("We cannot find this ID: "+connId+". Please try another one!");
        }
        this.connNum=this.connArray.size();
    }

    /**************************************
     *  private fields and constructor
     **************************************/

    //table of Connections
    private int idCounter=1;
    private String localIp="127.0.0.1";  //default
    private List<Connection> connArray=new ArrayList<Connection>();
    //the number of connections
    private int connNum=0;

    //the only static connectionManager object
    private static ConnectionManager theOnlyManager=new ConnectionManager();

    //static block: initialization
    static{

    }

    //private constructor: useless for creating static member
    private ConnectionManager(){}


    /*********************
     *  private methods
     *********************/

    //check if this connection id already exist
    //return the index if find the id, otherwise return -1
    private int hasId(int theId){
        int contains = -1;
        for(Connection ele : this.connArray){
            if (ele.id==theId){
                contains = connArray.indexOf(ele);
                break;
            }
        }
        return contains;
    }

    //check if a certain connetion is already exist
    //return the index if find the connection, otherwise return -1
    private int hasConn(String localIp, String directIp){
        int contains = -1;
        for(Connection ele : this.connArray){
            if (ele.myIp==localIp && ele.directIp==directIp){
                contains = connArray.indexOf(ele);
            }
        }
        return contains;
    }

    /**
     *  main test method: for the tests
     *  @param args void
     */
    public static void main(String[] args){

        //create connection
        ConnectionManager.singleton().addConn("234.52.234.34");
        ConnectionManager.singleton().addConn("234.52.234.34");
        ConnectionManager.singleton().addConn("34.342.54.345");
        ConnectionManager.singleton().addConn("86.3.34.423.4");
        ConnectionManager.singleton().addConn("37.356.7.4");

        //reset current local IP
        ConnectionManager.singleton().setLocalIp("192.168.2.1");

        //create some connections again
        ConnectionManager.singleton().addConn("37.565.25.2");
        ConnectionManager.singleton().addConn("678.4.13.889");
        ConnectionManager.singleton().addConn("565.785.243.676");
        ConnectionManager.singleton().addConn("797.224.666.234");
        ConnectionManager.singleton().addConn("45.23.4.85");

        //kill connection
        ConnectionManager.singleton().killConn(34);
        ConnectionManager.singleton().killConn(3);
        ConnectionManager.singleton().killConn(8);

        //show all connection
        ConnectionManager.singleton().showConn();
    }
}


/**
 *  The Connection class is only accessable in this package.
 */
class Connection {
    /**
     *  important fields of a connection
     */
    int id;
    String myIp;
    String directIp;
    Date birthday;

    //simple constructor. Private, no one can create the connection except me!
    Connection(int connId, String localIp, String clientIp){
        this.id=connId;
        this.myIp=localIp;
        this.directIp=clientIp;
        this.birthday=new Date();
    }

    //Full constructor. Private, no one can create the connection except me!
    Connection(int connId, String localIp, String clientIp, Date timeNow){
        this.id=connId;
        this.myIp=localIp;
        this.directIp=clientIp;
        this.birthday=timeNow;
    }

    //the toString() method in Object class is public, it cannot be protected or friendly here.
    //prepare the info stream for the printer
    public String toString(){
        //to get the template of the date
        DateFormat dateFormat = new SimpleDateFormat("yyyy/MM/dd HH:mm:ss");

        String result="  ID: "+this.id+"  \t| Local IP: "+this.myIp+"\t\t| Direct IP: "+this.directIp+"  \t| Etablished At: "+dateFormat.format(this.birthday);
        return result;
    }

    /**
     *  main test method: nothing in it.
     *  @param args void
     */
    public static void main(String[] args){

    }
}

输出:

  1. 尝试10次建立连接,成功9次。1次拒绝,因为连接已存在。
  2. 尝试3次断开连接,成功2次。1次失败,因为ID不存在。
  3. 最后打印剩下的全部7个连接。 exercise8

练习 9

Exercise 9: (2) Create the following file in the access/local directory (presumably in your CLASSPATH):

// access/local/PackagedClass.java
package access.local;
class PackagedClass {
  public PackagedClass() {
    System.out.println("Creating a packaged class");
  }
}

Then create the following file in a directory other than access/local:

// access/foreign/Foreign.java
package access.foreign;
import access.local.*;
public class Foreign {
   public static void main(String[] args) {
      PackagedClass pc = new PackagedClass();
   }
}

这是因为PackagedClass类没有设成public,只是包可见。所以包外的Foreign类无法创建它的实例。把Foreign类放到access/local包里,问题就解决了。